package sequence

import (
	"encoding/binary"
	"errors"
	"hash/crc32"
	"log"
	"os"
	"sync"
)

const (
	RestartGap uint64 = 1000
)

type Sequencer struct {
	snapshotPath string
	snapshotFile *os.File
	maxId        uint64
	currentId    uint64
	sequenceLock sync.Mutex
}

func NewSequencer(path string, maxId uint64) (s *Sequencer, err error) {
	s = new(Sequencer)

	s.snapshotPath = path
	s.snapshotFile, err = os.OpenFile(s.snapshotPath, os.O_RDWR, 0666)
	if err != nil {
		return nil, err
	}

	s.maxId = maxId
	err = s.fetchSnapshot()

	if err == nil {
		s.currentId += RestartGap
		err = s.snapshot()
	}

	return s, err
}

func (s *Sequencer) NextId() uint64 {
	s.sequenceLock.Lock()
	defer s.sequenceLock.Unlock()

	s.currentId++

	if s.currentId > s.maxId {
		return 0
	}

	if s.currentId%RestartGap == 0 {
		err := s.snapshot()
		if err != nil {
			log.Fatalf("[ERROR] Failed to snapshot the sequence %s\n", err)
		}
	}

	return s.currentId
}

func (s *Sequencer) snapshot() error {
	buf := make([]byte, 12)
	binary.BigEndian.PutUint64(buf[:8], s.currentId)
	binary.BigEndian.PutUint32(buf[8:], crc32.ChecksumIEEE(buf[:8]))

	_, err := s.snapshotFile.WriteAt(buf, 0)
	if err == nil {
		err = s.snapshotFile.Sync()
	}

	return err
}

func (s *Sequencer) fetchSnapshot() error {
	buf := make([]byte, 12)
	_, err := s.snapshotFile.Read(buf)
	if err != nil {
		return err
	}

	s.currentId = binary.BigEndian.Uint64(buf[:8])
	if s.currentId == 0 {
		return nil
	}

	crc := crc32.ChecksumIEEE(buf[:8])
	storedCrc := binary.BigEndian.Uint32(buf[8:])
	if crc != storedCrc {
		err = errors.New("data corruption")
	}

	return err
}
